package jmine.tec.di.resolver.impl;

import java.util.HashSet;
import java.util.Set;
import java.util.Map.Entry;

import jmine.tec.annotations.Unmodifiable;
import jmine.tec.di.resolver.ObjectResolver;
import jmine.tec.di.scope.TypedScope;
import jmine.tec.di.type.TypeConverter;
import jmine.tec.di.type.TypeConvertionException;
import jmine.tec.di.wrapper.Wrapper;

/**
 * @author MarceloT
 */
@Unmodifiable
public abstract class AbstractManyObjectsResolver implements ObjectResolver {

    private final Class<?> desiredType;

    private final TypeConverter<?> valueTypeConverter, keyTypeConverter;

    private final Class<?> keyType;

    /**
     * @param desiredType type
     * @param typeConverter o conversor de tipos
     * @param keyType the type
     * @param keyTypeConverter key type converter
     */
    public AbstractManyObjectsResolver(Class<?> desiredType, final TypeConverter<?> typeConverter, Class<?> keyType,
            TypeConverter<?> keyTypeConverter) {
        super();
        this.valueTypeConverter = typeConverter;
        this.desiredType = desiredType;
        this.keyType = keyType;
        this.keyTypeConverter = keyTypeConverter;
    }

    /**
     * @param key the string
     * @return Object
     * @throws TypeConvertionException e
     */
    protected final Object convertKey(String key) throws TypeConvertionException {
        return this.keyTypeConverter.convert(key, this.keyType);
    }

    /**
     * @param properties {@link TypedScope}
     * @return boolean
     */
    public boolean isResolvable(TypedScope properties) {
        return properties != null;
    }

    /**
     * @param properties {@link TypedScope}
     * @param wrapper {@link Wrapper}
     * @param requiredComponentClass type
     * @return Object
     * @throws TypeConvertionException e
     */
    public Object resolveObject(TypedScope properties, Wrapper wrapper, Class<?> requiredComponentClass) throws TypeConvertionException {
        String[] collectAllMatching = this.collectAllMatching(properties);
        Object[] values = this.collectValuesConvertType(properties, collectAllMatching);
        Object[] keys = this.convertKeys(collectAllMatching);
        return wrapper.wrap(keys, values, this.desiredType);
    }

    /**
     * Converte varias chaves
     * 
     * @param collectAllMatching String[]
     * @return Object[]
     * @throws TypeConvertionException e
     */
    private Object[] convertKeys(String[] collectAllMatching) throws TypeConvertionException {
        if (this.keyType == String.class) {
            return collectAllMatching;
        }
        Object[] converted = new Object[collectAllMatching.length];
        for (int i = 0; i < converted.length; i++) {
            converted[i] = this.convertKey(collectAllMatching[i]);
        }
        return converted;
    }

    /**
     * Coleta os valores, convertendo para os tipo necessarios
     * 
     * @param properties as propriedades
     * @param collectAllMatching os nomes
     * @return Object[]
     * @throws TypeConvertionException e
     */
    protected final Object[] collectValuesConvertType(TypedScope properties, String[] collectAllMatching) throws TypeConvertionException {
        Object[] values = new Object[collectAllMatching.length];
        for (int i = 0; i < values.length; i++) {
            final String key = collectAllMatching[i];
            Object toWrap = this.getObjectConvertType(properties, key);
            values[i] = toWrap;
        }
        return values;
    }

    /**
     * @param properties {@link TypedScope}
     * @param key {@link String}
     * @return {@link Object}
     * @throws TypeConvertionException e
     */
    protected final Object getObjectConvertType(TypedScope properties, final String key) throws TypeConvertionException {
        final Object fromScope = properties.get(key, Object.class);
        Object toWrap;
        if (fromScope == null) {
            toWrap = null;
        } else if (this.desiredType.isInstance(fromScope)) {
            toWrap = fromScope;
        } else {
            toWrap = this.valueTypeConverter.convert(String.valueOf(fromScope), this.desiredType);
        }
        return toWrap;
    }

    /**
     * @param props {@link TypedScope}
     * @return array de String
     */
    protected final String[] collectAllMatching(TypedScope props) {
        final Set<String> set = this.collectRecurse(props, new HashSet<String>());
        return set.toArray(new String[set.size()]);
    }

    /**
     * @param props {@link TypedScope}
     * @param acc {@link Set}
     * @return {@link Set}
     */
    private Set<String> collectRecurse(TypedScope props, Set<String> acc) {
        if (props == null) {
            return acc;
        }
        for (Entry<String, Object> entry : props.getProperties().entrySet()) {
            if (this.matches(entry.getKey())) {
                acc.add(entry.getKey());
            }
        }
        return this.collectRecurse(props.getParent(), acc);
    }

    /**
     * @param name {@link String}
     * @return boolean
     */
    protected abstract boolean matches(String name);

    /**
     * {@inheritDoc}
     * 
     * @see jmine.tec.di.cache.TypeMatch#isTypeCompatible(java.lang.Class)
     */
    public boolean isTypeCompatible(Class<?> declaredRawType) {
        return declaredRawType != null && this.desiredType.isAssignableFrom(declaredRawType);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isStatic() {
        return false;
    }
}
